<!-- 吸顶 -->
<template>
  <view class="aster-sticky" :id="elId + ''" :style="style">
    <view :style="stickyContent" class="aster-sticky__content">
      <slot></slot>
    </view>
  </view>
</template>
<script setup lang="ts">
  import { addStyle, addUnit, deepMerge } from '@/utils';
  import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';

  // 获取组件实例
  const instance = getCurrentInstance();

  const props = defineProps({
    // 吸顶容器到顶部某个距离的时候，进行吸顶，在H5平台，NavigationBar为44px
    offsetTop: {
      type: Number,
      default: () => 0,
    },
    // 自定义导航栏的高度
    customNavHeight: {
      type: Number,
      default: () => {
        if (
          uni.getSystemInfoSync().platform.toLowerCase() === 'android' ||
          uni.getSystemInfoSync().platform.toLowerCase() === 'ios'
        ) {
          return 0; // 44
        } else {
          return 0;
        }
      },
    },
    // 是否开启吸顶功能
    disabled: {
      type: Boolean,
      default: () => false,
    },
    // 吸顶区域的背景颜色
    bgColor: {
      type: String,
      default: () => '#fff',
    },
    // z-index值
    zIndex: {
      type: Number,
      default: () => 970,
    },
    // 列表中的索引值
    index: {
      type: Number,
      default: () => 0,
    },
    customStyle: {
      type: Object,
      default: () => {},
    },
  });

  // 是否使用css的sticky实现
  const cssSticky: any = ref(false);
  // 吸顶的top值，因为可能受自定义导航栏影响，最终的吸顶值非offsetTop值
  const stickyTop = ref(0);
  const elId = ref(new Date().getTime);
  // js模式时，吸顶的内容因为处于postition: fixed模式，为了和原来保持一致的样式，需要记录并重新设置它的left，height，width属性
  const left = ref(0);
  const width = ref('auto');
  const height = ref('auto');
  // js模式时，是否处于吸顶模式
  const fixed = ref(false);

  const style = computed(() => {
    const style: any = {};
    if (!props.disabled) {
      if (cssSticky.value) {
        style.position = 'sticky';
        style.zIndex = props.zIndex;
        style.top = addUnit(stickyTop.value);
      } else {
        style.height = fixed.value ? height.value + 'rpx' : 'auto';
      }
    } else {
      // 无需吸顶时，设置会默认的relative(nvue)和非nvue的static静态模式即可
      // #ifdef APP-NVUE
      style.position = 'relative';
      // #endif
      // #ifndef APP-NVUE
      style.position = 'static';
      // #endif
    }
    style.backgroundColor = props.bgColor;
    return deepMerge(addStyle(props.customStyle), style);
  });

  // 吸顶内容的样式
  const stickyContent = computed(() => {
    const style: any = {};
    if (!cssSticky.value) {
      style.position = fixed.value ? 'fixed' : 'static';
      style.top = stickyTop.value + 'rpx';
      style.left = left.value + 'rpx';
      style.width = width.value == 'auto' ? 'auto' : width.value + 'rpx';
      style.zIndex = props.zIndex;
    }
    return style;
  });

  onMounted(() => {
    init();
  });

  const init = () => {
    getStickyTop();
    // 判断使用的模式
    checkSupportCssSticky();
    // 如果不支持css sticky，则使用js方案，此方案性能比不上css方案
    if (!cssSticky.value) {
      !props.disabled && initObserveContent();
    }
  };
  const initObserveContent = () => {
    // 获取吸顶内容的高度，用于在js吸顶模式时，给父元素一个填充高度，防止"塌陷"
    uGetRect('#' + elId.value).then((res: any) => {
      height.value = res.height;
      left.value = res.left;
      width.value = res.width;
      nextTick(() => {
        observeContent();
      });
    });
  };
  const uGetRect = (selector: any, all?: any) => {
    return new Promise((resolve) => {
      uni
        .createSelectorQuery()
        .in(instance)
        [all ? 'selectAll' : 'select'](selector)
        .boundingClientRect((rect) => {
          if (all && Array.isArray(rect) && rect.length) {
            resolve(rect);
          }
          if (!all && rect) {
            resolve(rect);
          }
        })
        .exec();
    });
  };
  const contentObserver = ref();
  const observeContent = () => {
    // 先断掉之前的观察
    disconnectObserver();
    const observer = uni.createIntersectionObserver({
      // 检测的区间范围
      thresholds: [0.95, 0.98, 1],
    });
    // 到屏幕顶部的高度时触发
    observer.relativeToViewport({
      top: -stickyTop.value,
    });
    // 绑定观察的元素
    observer.observe(`#${elId.value}`, (res) => {
      setFixed(res.boundingClientRect.top);
    });
    contentObserver.value = observer;
  };
  const setFixed = (top: number) => {
    // 判断是否出于吸顶条件范围
    const f = top <= stickyTop.value;
    fixed.value = f;
  };
  const disconnectObserver = () => {
    // 断掉观察，释放资源
    const observer = contentObserver.value;
    observer && observer.disconnect();
  };
  const getStickyTop = () => {
    stickyTop.value = props.offsetTop + props.customNavHeight;
  };
  const checkSupportCssSticky = async () => {
    // #ifdef H5
    // H5，一般都是现代浏览器，是支持css sticky的，这里使用创建元素嗅探的形式判断
    if (checkCssStickyForH5()) {
      cssSticky.value = true;
    }
    // #endif

    // 如果安卓版本高于8.0，依然认为是支持css sticky的(因为安卓7在某些机型，可能不支持sticky)
    if (
      uni.getSystemInfoSync().platform.toLowerCase() === 'android' &&
      Number(uni.getSystemInfoSync().system) > 8
    ) {
      cssSticky.value = true;
    }

    // APP-Vue和微信平台，通过computedStyle判断是否支持css sticky
    // #ifdef APP-VUE || MP-WEIXIN
    cssSticky.value = await checkComputedStyle();
    // #endif

    // ios上，从ios6开始，都是支持css sticky的
    if (uni.getSystemInfoSync().platform.toLowerCase() === 'ios') {
      cssSticky.value = true;
    }

    // nvue，是支持css sticky的
    // #ifdef APP-NVUE
    cssSticky.value = true;
    // #endif
  };
  // 在APP和微信小程序上，通过uni.createSelectorQuery可以判断是否支持css sticky
  const checkComputedStyle = () => {
    // 方法内进行判断，避免在其他平台生成无用代码
    // #ifdef APP-VUE || MP-WEIXIN
    const query = uni.createSelectorQuery().in(instance);
    return new Promise((resolve) => {
      query
        .select('.aster-sticky')
        .fields(
          {
            computedStyle: ['position'],
          },
          () => {},
        )
        .exec((e) => {
          resolve('sticky' === e[0].position);
        });
    });
    // #endif
  };
  // H5通过创建元素的形式嗅探是否支持css sticky
  // 判断浏览器是否支持sticky属性
  const checkCssStickyForH5 = () => {
    // 方法内进行判断，避免在其他平台生成无用代码
    // #ifdef H5
    const vendorList = ['', '-webkit-', '-ms-', '-moz-', '-o-'],
      vendorListLength = vendorList.length,
      stickyElement = document.createElement('div');
    for (let i = 0; i < vendorListLength; i++) {
      stickyElement.style.position = vendorList[i] + 'sticky';
      if (stickyElement.style.position !== '') {
        return true;
      }
    }
    return false;
    // #endif
  };

  onBeforeUnmount(() => {
    disconnectObserver();
  });
</script>
<style lang="scss" scoped>
  .aster-sticky {
    /* #ifdef APP-VUE || MP-WEIXIN */
    // 此处默认写sticky属性，是为了给微信和APP通过uni.createSelectorQuery查询是否支持css sticky使用
    position: sticky;
    /* #endif */

    &__content {
      display: flex;
      justify-content: flex-start;
      align-items: center;
    }
  }
</style>
